/*
 * temperature.c
 *
 * Copyright 2011 Pieter Agten
 *
 * This file is part of Yarf.
 *
 * Yarf is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Yarf is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Yarf.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * @file   temperature.c
 * @author Pieter Agten (pieter.agten@gmail.com)
 * @date   9 okt 2011
 * @brief  The temperature component manages the temperature of the nozzle 
 *         and the printbed.
 */

#include "temperature.h"

#include "yarf.h"
#include "thermistor_table.h"
#include "scheduling/periodic.h"
#include "util/math.h"
#include "pid.h"

#include <stdint.h>
#include <avr/interrupt.h> 
#include <util/atomic.h> 

/**
 * The target temperature of the nozzle and printbed right after initializing
 * the temperature component, in degrees celsius.
 */
#define INITIAL_TARGET_TEMP_C 0


/**
 * Declaration of a datastructure representing a heater.
 */
struct heater {
  pid_t pid;                     /**< PID algorithm instance that determines the amount of power to apply to the heater. */
  float max_temp;                /**< Absolute maximum temperature the heater can be set to, in degrees celsius. */
  void (*set_power)(uint8_t dc); /**< Pointer to a function that sets the relative amount of power applied to the heater. */
  volatile uint16_t current_adc; /**< Last measured ADC value from the thermistor measuring the heater's temperature. */
  volatile float current_temp;   /**< Last measured temperature of the heater (based on \a current_adc). */
  uint8_t current_power;         /**< The relative amount of power currently applied to the heater (from 0 to 255). */
};

/**
 * Heater datastructure instance for the printhead nozzle.
 */
static struct heater nozzle;

/**
 * Heater datastructure instance for the heated printbed.
 */
static struct heater printbed;

/**
 * Enumeration representing the states the ADC manager can be in.
 */
static volatile enum {
  IDLE,              /**< The ADC manager is not doing anything. */
  READING_NOZZLE,    /**< The ADC manager is reading the nozzle's temperature. */
  READING_PRINTBED   /**< The ADC manager is reading the printbed's temperature. */
} adc_state;


/**
 * Periodic task for checking and controlling the nozzle and printbed
 * temperatures at constant intervals.
 */
static periodic_task_t temperature_task;


/**
 * Sets the nozzle heater's PWM duty cycle.
 *
 * @param dc  the duty cycle to which to set the nozzle heater's PWM module
 *            (0 to 255)
 *
 * @note The nozzle heater's PWM duty cycle is never set higher than the \a
 *       NOZZLE_HEATER_MAX_DUTY_CYCLE configuration parameter.
 */
static void
set_nozzle_duty_cycle(uint8_t dc)
{
  NOZZLE_HEATER_PWM_REG = MIN(dc,NOZZLE_HEATER_MAX_DUTY_CYCLE);
}

/**
 * Sets the printbed heater's PWM duty cycle.
 *
 * @param dc  the duty cycle to which to set the printbed heater's PWM module
 *            (0 to 255)
 *
 * @note The printbed heater's PWM duty cycle is never set higher than the \a
 *       PRINTBED_HEATER_MAX_DUTY_CYCLE configuration parameter.
 */
static inline void
set_printbed_duty_cycle(uint8_t dc)
{
  PRINTBED_HEATER_PWM_REG = MIN(dc,PRINTBED_HEATER_MAX_DUTY_CYCLE);
}

/**
 * Initializes the hardware ADC component.
 */
static inline void
adc_init(void)
{
  /* Disable digital inputs on ADC pins for reducing power consumption */
  DIDR0 = _BV(TEMP1_PIN) | _BV(TEMP2_PIN);
  
  /* Set voltage reference */
  ADMUX = (1 << REFS0);

  /*@brief  The temperature component manages the temperature of the nozzle 
 *         and the printbed. Set ADC prescaler and enable ADC */
  ADCSRA = _BV(ADEN) | _BV(ADIE) | (7 << ADPS0);
}

/**
 * Initializes the hardware PWM component.
 */
static inline void
pwm_init(void)
{
  /* Disable all timer 0 interrupts */
  TIMSK0 = 0;

  /* Set duty cycle to 0 */
  OCR0A = 0;
  OCR0B = 0;

  /* Select Compare Output Mode, 1024 prescaler, phase correct PWM */
  TCCR0A = (2 << COM0A0) | (2 << COM0B0) | (1 << WGM00);
  TCCR0B = (5 << CS00);
  // PWM freq = F_CPU/1024/255/2
}

/**
 * Sets the amount of power applied to a given heater, based on the result of
 * calling the heater's PID instance. This function turns the heater off, if
 * its temperature exceeds its maximum temperature.
 *
 * @param heater  pointer to the heater structure for which to manage the power
 */
static void
manage_heater_power(struct heater *heater)
{
  uint8_t power;
  if (heater->current_temp < heater->max_temp) {
    power = pid_next(&(heater->pid), heater->current_temp);
  } else {
    power = 0;
  }
  heater->set_power(power);
  heater->current_power = power;
}

/**
 * Returns the temperature for a given nozzle thermistor ADC value, based on
 * the nozzle thermistor table.
 *
 * @param adc the 10-bit ADC value measured from the nozzle's thermistor
 */
static inline float
nozzle_temperature(uint16_t adc)
{
  return pgm_read_float(&(NOZZLE_THERMISTOR_TABLE[adc]));
}

/**
 * Returns the temperature for a given printbed thermistor ADC value, based on
 * the printbed thermistor table.
 *
 * @param adc the 10-bit ADC value measured from the printbed's thermistor
 */
static inline float
printbed_temperature(uint16_t adc)
{
  return pgm_read_float(&(PRINTBED_THERMISTOR_TABLE[adc]));
}

/**
 * ADC conversion complete interrupt service routine
 *
 * This procedure is called when an ADC conversion is complete. After reading
 * and managing the nozzle's temperature, the printbed's temperature is read
 * and managed. All ADC conversions are performed asynchronously.
 */
ISR(ADC_vect,ISR_NOBLOCK)
{
  uint16_t adc_value;
  ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
    adc_value = ADCL;
    adc_value |= (ADCH << 8);
  }
  switch (adc_state) {
    case READING_NOZZLE:
      nozzle.current_adc = adc_value;
      nozzle.current_temp = nozzle_temperature(adc_value);
      manage_heater_power(&nozzle);

      /* Select the printbed thermistor pin */
      ADMUX = (ADMUX & ~(0x1F << MUX0)) | (PRINTBED_THERMISTOR_PIN << MUX0);

      /* Start the ADC conversion (asynchronously) */
      adc_state = READING_PRINTBED;
      ADCSRA |= _BV(ADSC);
      break;
    case READING_PRINTBED:
      printbed.current_adc = adc_value;
      printbed.current_temp = printbed_temperature(adc_value);
      manage_heater_power(&printbed);

      adc_state = IDLE;
      break;
    default:
      break;
  }
}

/**
 * Starts an asynchronous ADC reading for the nozzle thermistor. After the 
 * reading is completed, the \a ADC_vect interrupt service routine will be
 * executed. This function should be called periodically to manage the nozzle
 * and printbed temperatures.
 */
static void
temp_periodic(void)
{
  /* Make sure the previous adc conversion has been completed */
  if (adc_state != IDLE) {
    /* ADC can't keep up! */
    return;
  }

  /* Select the nozzle thermistor pin */
  ADMUX = (ADMUX & ~(0x1F << MUX0)) | (NOZZLE_THERMISTOR_PIN << MUX0);

  /* Start the ADC conversion (asynchronously) */
  adc_state = READING_NOZZLE;
  ADCSRA |= _BV(ADSC);
}


/**
 * Returns whether a heater's target temperature has been reached. This is 
 * considered to be the cases when its temperature is within 2 degrees celsius
 * of the target temperature.
 *
 * @param h  pointer to the heater for which to check if the target temperature
 *           has been reached.
 * @return True if the heater's target temperature has been reached and false
 *         otherwise.
 */
//TODO: improve this method
static inline bool
temperature_reached(struct heater *h)
{
  return h->pid.target_temp == 0 ||
         fabs(h->current_temp - h->pid.target_temp) < 2;
}

void
temp_init(void)
{
  adc_init();
  pwm_init();

  nozzle.max_temp = NOZZLE_HEATER_MAX_TEMP;
  nozzle.set_power = set_nozzle_duty_cycle;
  pid_init(&nozzle.pid, NOZZLE_PID_ENVELOPE, NOZZLE_PID_P_GAIN, NOZZLE_PID_I_GAIN, NOZZLE_PID_D_GAIN);
  pid_set_target(&nozzle.pid, INITIAL_TARGET_TEMP_C);
  
  printbed.max_temp = PRINTBED_HEATER_MAX_TEMP;
  printbed.set_power = set_printbed_duty_cycle;
  pid_init(&printbed.pid, PRINTBED_PID_ENVELOPE, PRINTBED_PID_P_GAIN, PRINTBED_PID_I_GAIN, PRINTBED_PID_D_GAIN);
  pid_set_target(&printbed.pid, INITIAL_TARGET_TEMP_C);

  periodic_task_set(&temperature_task, temp_periodic, (int)(1000.0/HEATER_MANAGEMENT_FREQUENCY_HZ));
  periodic_add(&temperature_task);
}


/* ********** Nozzle ********** */
void
temp_set_nozzle_target(float c)
{
  pid_set_target(&nozzle.pid, c);
}


float
temp_get_nozzle_temp(void)
{
  return nozzle.current_temp;
}


uint16_t
temp_get_nozzle_adc(void)
{
  return nozzle.current_adc;
}


uint8_t
temp_get_nozzle_power(void)
{
  return nozzle.current_power;
}

bool
temp_nozzle_temperature_reached(void)
{
  return temperature_reached(&nozzle);
}

void
temp_nozzle_temperature_wait(void)
{
  while (! temp_nozzle_temperature_reached()) {
  }
}

/* ********** Printbed ********** */
void
temp_set_printbed_target(float c)
{
  pid_set_target(&printbed.pid, c);
}


float
temp_get_printbed_temp(void)
{
  return printbed.current_temp;
}


uint8_t
temp_get_printbed_power(void)
{
  return printbed.current_power;
}

bool
temp_printbed_temperature_reached(void)
{
  return temperature_reached(&printbed);
}

void
temp_printbed_temperature_wait(void)
{
  while (! temp_printbed_temperature_reached()) {
  }
}

